; BIOS的程序加载器，用来在实模式下加载操作系统
; 硬盘主引导扇区代码

        app_lba_start equ 100       ; 声明常数（用户程序起始逻辑扇区号）

SECTION mbr align=16 vstart=0x7c00  ; 主引导扇区默认是在0x7c00开始
                                    ; 通过vstart在编译器实际计算物理内存地址的时候会加上这个偏移量

        mov ax, 0                   ; 设置栈帧寄存器和栈指针
        mov ss, ax
        mov sp, ax

        mov ax, [cs:phy_base]       ; 计算用于加载用户程序的逻辑段地址
        mov dx, [cs:phy_base+0x02]

        ; 尽量避免除法运算，因为除法运算会消耗通常会消耗更多的机器周期
        shr ax, 4                   ; 计算得到段地址
        shl dx, 12
        and dx, 0xf000
        or ax, dx
        ;mov bx, 16
        ;div bx

        mov ds, ax                  ; 令DS和ES指向改段以进行操作
        mov es, ax

        ; 一下读取程序的起始部分
        xor di, di
        mov si, app_lba_start       ; 程序在硬盘上的起始逻辑扇区号
        xor bx, bx                  ; 记载到DS:0x0000处
        call read_hard_disk_0

        mov dx, [2]
        mov ax, [0]
        mov bx, 512
        div bx
        cmp dx, 0
        jnz @1
        dec ax

    @1:
        cmp ax, 0                   ; 程序的实际长度小于等于512个字节的情况
        jz direct

        push ds                     ; 保存上下文

        mov cx, ax                  ; 循环读取的次数（剩余扇区数）

    @2:
        mov ax, ds
        add ax, 0x20
        mov ds, ax

        xor bx, bx                  ; 每次读时，偏移地址始终为0x0000
        inc si                      ; 下一个逻辑扇区
        call read_hard_disk_0       
        loop @2                     ; 循环读，知道读完整个功能程序

        pop ds                      ; 恢复数据段基址到用户程序头部段        

    ; 计算入口点代码段基地址
    direct:
        mov dx, [0x08]
        mov ax, [0x06]
        call calc_segment_base
        mov [0x06], ax              ; 回填修正后的入口点代码段基址

    ; 开始处理段重定位表
        mov cx, [0x0a]              ; 需要重定位的项目数量
        mov bx, 0x0c                ; 重定位表首地址
realloc:
        mov dx, [bx+0x02]
        mov ax, [bx]
        call calc_segment_base
        mov [bx], ax
        add bx, 4
        loop realloc

        jmp far [0x04]              ; 转移到用户程序

;----------------------------------------------------------------
read_hard_disk_0:                   ; 从硬盘读取一个逻辑扇区号
                                    ; 参数 :
                                    ; (1) DI:SI => 起始逻辑扇区号
                                    ; (2) DS:BX => 目标缓冲区地址

        push ax
        push bx
        push cx
        push dx

        mov dx, 0x1f2
        mov al, 1
        out dx, al                  ; 读取的扇区数

        inc dx                      ; 0x1f3
        mov ax, si
        out dx, al                  ; LBA地址7~0

        inc dx                      ; 0x1f4
        mov al, ah
        out dx, al                  ; LBA地址15~8

        inc dx                      ; 0x1f5
        mov ax, di
        out dx, al                  ; LBA地址23~16

        inc dx
        mov al, 0xe0                ; LBA28模式，主盘
        or al, ah                   ; LBA地址27~24
        out dx, al

        inc dx                      ; 0x1f7
        mov al, 0x20                ; 读命令
        out dx, al

    ; 轮询检查磁盘io是否结束
    .waits:
        in al, dx
        and al, 0x88
        cmp al, 0x08
        jnz .waits                  ; 硬盘准备号数据传输

    ;连续取出数据
        mov cx, 256                 ; 总共要读取的字节数
        mov dx, 0x1f0

    .readw:
        in ax, dx
        mov [bx], ax
        add bx, 2
        loop .readw

        pop dx
        pop cx
        pop bx
        pop ax

        ret

;----------------------------------------------------------------
calc_segment_base:                  ; 计算16位段地址（实际运行时程序的段地址）
    ; 调用参数=> dx|ax 16位相对段地址
        
        push dx

        add ax, [cs:phy_base]       ; 这里的[cs:phy_base] => [cs:phy_base+vstart]
        adc dx, [cs:phy_base+0x02]  ; 带进位的加法，用于完成32位数的加法运算
        shr ax, 4                   ; 将ax中的值右移四位
        ; 合并出逻辑段地址(dx的低4位和ax的高12位)
        shl dx, 12
        or ax, dx                   

        pop dx
        ret                         ; 返回

;----------------------------------------------------------------
        phy_base dd 0x10000         ; 用户程序被加载的物理起始地址

times 510-($-$$)    db 0
                    db 0x55, 0xaa   ; 主引导扇区结束的标志
